/*
* This file is a part of the open-eBackup project.
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at
* http://mozilla.org/MPL/2.0/.
*
* Copyright (c) [2024] Huawei Technologies Co.,Ltd.
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
*/
#include "HCSTokenMgr.h"
#include <thread>
#include <regex>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
#include "log/Log.h"
#include "protect_engines/hcs/common/HcsHttpStatus.h"
#include "protect_engines/hcs/api/iam/IamClient.h"
#include "common/token_mgr/GetTokenResponse.h"
#include "protect_engines/hcs/utils/HcsOpServiceUtils.h"

using namespace std;
namespace {
const std::string MODULE_NAME = "HCSTokenMgr";
const std::string KEY_TOKEN_PREFIX_DOMAIN = "domain_";
const std::string KEY_TOKEN_PREFIX_PROJECT = "project_";
const std::string KEY_TOKEN_PREFIX_ADMIN_PROJECT = "admin_project_";
}

HCS_PLUGIN_NAMESPACE_BEGIN

static std::once_flag startCheckTokenThread;

HCSTokenMgr& HCSTokenMgr::GetInstance()
{
    static HCSTokenMgr instance;
    std::call_once(startCheckTokenThread, [&]() {
        std::thread thread(&HCSTokenMgr::CheckToken, &instance);
        thread.detach();
    });
    return instance;
}

std::string HCSTokenMgr::GetTokenKey(ModelBase &model)
{
    std::string key;
    std::string scopeValue = model.GetScopeValue();
    if (model.GetScopeType() == Scope::USER_DOMAIN) {
        key = model.GetEnvAddress() + std::string(KEY_TOKEN_PREFIX_DOMAIN) + scopeValue + std::string("_") + \
                model.GetUserInfo().name;
    } else if (model.GetScopeType() == Scope::PROJECT) {
        key = model.GetEnvAddress() + std::string(KEY_TOKEN_PREFIX_PROJECT) + scopeValue;
    } else {
        key = model.GetEnvAddress() + std::string(KEY_TOKEN_PREFIX_ADMIN_PROJECT) + scopeValue;
    }
    return key;
}

bool HCSTokenMgr::ReacquireToken(ModelBase &model, std::string &tokenValue)
{
    std::lock_guard<std::mutex> locker(m_tokenMutex);
    // 重新查询token
    GetTokenRequest getTokenRequeset;
    InitGetTokenRequest(getTokenRequeset, model);
    IamClient iamClient;
    std::shared_ptr<GetTokenResponse> getTokenResponse = std::make_shared<GetTokenResponse>();

    if (!getTokenRequeset.GetToken().empty()) {
        DBGLOG("Get token in hcs opservice env.");
        getTokenResponse = iamClient.VerifyToken(getTokenRequeset);
    } else if (getTokenRequeset.GetTokenType() == Scope::ADMIN_PROJECT) {
        DBGLOG("Get admin token of project.");
        getTokenResponse = iamClient.GetAdminTokenOfProject(getTokenRequeset, model);
    } else {
        DBGLOG("Get token.");
        getTokenResponse = iamClient.GetToken(getTokenRequeset);
    }
    if ((getTokenResponse == nullptr) ||
        (getTokenResponse->GetStatusCode() != static_cast<uint32_t>(HcsExternalStatusCode::CREATEED) &&
            getTokenResponse->GetStatusCode() != static_cast<uint32_t>(HcsExternalStatusCode::OK))) {
        ERRLOG("Failed to get token by iam api.");
        return false;
    }
    if (!getTokenResponse->Serial()) {
        ERRLOG("Failed to serial token requeset.");
        return false;
    }
    tokenValue = AddToken(model, getTokenResponse);
    if (tokenValue.empty()) {
        ERRLOG("Failed to save token.");
        return false;
    }
    return true;
}

void HCSTokenMgr::InitGetTokenRequest(GetTokenRequest &getTokenRequeset, ModelBase &model)
{
    getTokenRequeset.SetTokenType(model.GetScopeType());
    getTokenRequeset.SetUserInfo(model.GetIamUserInfo());
    getTokenRequeset.SetScopeValue(model.GetScopeValue());
    getTokenRequeset.SetDomain(model.GetDomain());
    getTokenRequeset.SetEndpoint(model.GetEndpoint());
    getTokenRequeset.SetRegion(model.GetRegion());
    getTokenRequeset.SetEnvAddress(model.GetEnvAddress());
    getTokenRequeset.SetToken(model.GetToken());
    getTokenRequeset.SetNeedRetry(false);
    getTokenRequeset.SetExtendInfo(model.GetExtendInfo());
}

bool HCSTokenMgr::ParseEndpoint(ModelBase &model, TokenInfo &tokenInfo, std::string &endPoint)
{
    std::map<ApiType, std::string> apiMap = {{ApiType::ECS, "ecs"}, {ApiType::EVS, "evs"},
        {ApiType::CINDER, "cinder"}, {ApiType::NOVA, "nova"}};
    auto it = apiMap.find(model.GetApiType()); // 根据查找结果，确定是否需要解析endpoint
    if (it == apiMap.end()) {
        DBGLOG("Not need to parse endpoint.");
        return true;
    }
    std::string apiType = it->second;
    std::string regionId = model.GetRegion();
    DBGLOG("Parse endpoint, api type:%s regionId:%s", apiType.c_str(), regionId.c_str());

    TokenDetail tokenDetail;
    if (!Module::JsonHelper::JsonStringToStruct(tokenInfo.m_extendInfo, tokenDetail)) {
        ERRLOG("Failed to trans string to tokenDetail.");
        return false;
    }
    std::string endpointStr = "";
    std::vector<Catalog> catalog = tokenDetail.m_token.m_catalog;
    for (int i = 0; i < catalog.size(); i++) {
        Catalog curCatalog = catalog[i];
        if (curCatalog.m_name != apiType) {
            continue;
        }
        std::vector<Endpoint> endpoints = curCatalog.m_endpoints;
        for (int j = 0; j < endpoints.size(); j++) {
            Endpoint curEndpoint = endpoints[j];
            if (curEndpoint.m_regionId == regionId) {
                endpointStr = curEndpoint.m_url;
                /* 8.1.0 return incorrect cinder url, correct it here */
                ReplaceProjectId(apiType, model.GetScopeValue(), endpointStr, !model.GetToken().empty());
                break;
            }
        }
    }
    if (endpointStr.empty()) {
        if (GetEndpoints(tokenInfo, model, apiType, endPoint) == SUCCESS) {
            return true;
        }
        ERRLOG("Failed to get endpoint.");
        return false;
    }
    endPoint = endpointStr;
    return true;
}

int32_t HCSTokenMgr::GetEndpoints(TokenInfo &tokenInfo, ModelBase &model, const std::string &apiType, std::string &endPoint)
{
    if (ParseApiEndpoint(model, tokenInfo, apiType, endPoint) == SUCCESS) {
        return SUCCESS;
    }
    IamClient iamClient;
    std::shared_ptr<ResponseModel> getEndpointsResponse = std::make_shared<ResponseModel>();
    getEndpointsResponse = iamClient.GetEndpoints(model, tokenInfo.m_token);
    if (getEndpointsResponse == nullptr || getEndpointsResponse->GetStatusCode() != static_cast<uint32_t>(HcsExternalStatusCode::OK)) {
        ERRLOG("Failed to get endpoints by iam api.");
        return FAILED;
    }
    tokenInfo.m_apiEndpointStr = getEndpointsResponse->GetBody();
    if (ParseApiEndpoint(model, tokenInfo, apiType, endPoint) != SUCCESS) {
        ERRLOG("Failed to parse endpoint.");
        return FAILED;
    }
    return SUCCESS;
}

int32_t HCSTokenMgr::ParseApiEndpoint(ModelBase &model, const TokenInfo &tokenInfo, const std::string &apiType, std::string &endPoint)
{
    Catalog catalog;
    if (!Module::JsonHelper::JsonStringToStruct(tokenInfo.m_apiEndpointStr, catalog)) {
        ERRLOG("Failed to trans string to catalog.");
        return FAILED;
    }
    std::map<ApiType, std::string> apiMap = {{ApiType::ECS, "//ecs."}, {ApiType::EVS, "//evs."},
        {ApiType::CINDER, "//volume."}, {ApiType::NOVA, "//compute."}};
    auto it = apiMap.find(model.GetApiType()); // 根据查找结果，确定是否需要解析endpoint
    if (it == apiMap.end()) {
        ERRLOG("Not need to parse endpoint.");
        return FAILED;
    }
    std::string apiUrl = it->second;
    std::string regionId = model.GetRegion();
    std::string endpointStr = "";
    for (const Endpoint &endpointItem : catalog.m_endpoints) {
        if (endpointItem.m_regionId != regionId) {
            continue;
        }
        if (endpointItem.m_url.find(apiUrl) != std::string::npos) {
            endpointStr = endpointItem.m_url;
            ReplaceProjectId(apiType, model.GetScopeValue(), endpointStr, !model.GetToken().empty());
            ReplaceTenantId(apiType, model.GetScopeValue(), endpointStr);
            break;
        }
    }
    if (endpointStr.empty()) {
        ERRLOG("Failed to get endpoint.");
        return FAILED;
    }
    endPoint = endpointStr;
    return SUCCESS;
}

void HCSTokenMgr::ReplaceTenantId(const std::string apiType, const std::string &newId, std::string &oldurl)
{
    if (oldurl.find("$(tenant_id)s") == std::string::npos) {
        return;
    }
    INFOLOG("Before replace, url: %s, new project id: %s", oldurl.c_str(), newId.c_str());
    static const std::regex e("^[0-9a-z]{32}$");
    if (newId.empty() || !std::regex_match(newId, e)) {
        WARNLOG("New project id is not meet the rule, not to replace. Project id: %s", newId.c_str());
        return;
    }
    auto pos = oldurl.find("$(tenant_id)s");
    std::string newUrl = oldurl.substr(0, pos) + newId;
    INFOLOG("After replace: %s", newUrl.c_str());
    oldurl = newUrl;
}

void HCSTokenMgr::ReplaceProjectId(const std::string apiType, const std::string &newId, std::string &oldurl,
    bool isOpService)
{
    if (isOpService) {
        int32_t headPos = oldurl.find_last_of('/');
        std::string oldUrlHead = oldurl.substr(0, headPos + 1);
        oldurl = oldUrlHead + newId;
        return;
    }
    if (apiType != "cinder" && apiType != "nova") {
        DBGLOG("Not cinder url, not to replace.");
        return;
    }

    INFOLOG("Before replace, url: %s, new project id: %s", oldurl.c_str(), newId.c_str());
    static const std::regex e("^[0-9a-z]{32}$");
    if (newId.empty() || !std::regex_match(newId, e)) {
        WARNLOG("New project id is not meet the rule, not to replace. Project id: %s", newId.c_str());
        return;
    }

    // url: https://volume.az.xxxxx.com:443/v2/b17cd454250e4f0eb5d75330a0a29efe
    std::string url = oldurl;
    auto pos = url.find_last_of('/');
    auto oldProjId = url.substr(pos + 1);
    while (oldProjId.empty()) {
        url = url.substr(0, pos);
        pos = url.find_last_of('/');
        oldProjId = url.substr(pos + 1);
    }
    INFOLOG("Got project id from cinder url, project id: %s", oldProjId.c_str());
    if (oldProjId == newId) {
        INFOLOG("Same project id, not to replace.");
        return;
    }
    if (oldProjId.length() != newId.length() || !std::regex_match(oldProjId, e)) {
        WARNLOG("Project id from cinder url not meet the regex rule, not to replace.");
        return;
    }
    url.replace(pos + 1, oldProjId.length(), newId);
    oldurl = url;
    INFOLOG("After replace: %s", url.c_str());
}

HCS_PLUGIN_NAMESPACE_END